/*
 * CPU.java
 * 
 * Team Dovahkiin:
 * Major Bruno, Matthew Selfors
 * 
 * This class simulates the CPU for this SC-2 simulator.
 */

package phaseII;

import java.math.BigInteger;
import java.util.Random;

import phaseI.Alu;
import phaseI.Memory;
import phaseI.Register;
import phaseI.RegisterFile;
import phaseIII.ConsoleInput;

/**
 * This class simulates the CPU and its fetch/decode/execute cycle
 * for the SC-2 simulator.
 * 
 * @author Major Bruno
 * @author Matthew Selfors
 * @version 1.0
 */
public class CPU {

	/**
	 * An array of hex values for conversion purposes.
	 */
	private static final String[] HEX = { "0", "1", "2", "3", "4", "5", "6",
			"7", "8", "9", "A", "B", "C", "D", "E", "F" };

	/**
	 * An array of binary values for conversion purposes.
	 */
	private static final String[] BINARY = { "0000", "0001", "0010", "0011",
			"0100", "0101", "0110", "0111", "1000", "1001", "1010", "1011",
			"1100", "1101", "1110", "1111" };

	/**
	 * The maximum value a register can contain.
	 */
	private static final int MAX_REG_VALUE = 32767;
	
	private static final int MEM_SIZE = 65536;

	/**
	 * The maximum value a memory address can contain.
	 */
	private static final int MAX_MEM_VALUE = 127;

	/**
	 * Number of registers.
	 */
	private static final int REG_COUNT = 16;
	
	/**
	 * Number of registers in the ALU.
	 */
	private static final int ALU_REG = 5;
	
	/**
	 * DEST_START is the starting point in the binary string for
	 * the destination register. DEST_END is the end point.
	 * ADDR_END is the ending point in the binary string for the
	 * address register.
	 */
	private static final int DEST_START = 5, DEST_END = 9, ADDR_END = 13;

	/**
	 * The Register File for the simulator.
	 */
	public RegisterFile reg_file;

	/**
	 * The Registers for the Status Word, Program Counter, Instruction Register,
	 * Memory Data Register, Memory Address Register, and Operand Registers
	 * respectively.
	 */
	public Register sw, pc, ir, mdr, mar, op1, op2;

	/**
	 * The Arithmetic Logic Unit for the simulator.
	 */
	public Alu alu;

	/**
	 * The Memory for the simulator.
	 */
	public Memory mem;

	/**
	 * The current macro state for the fetch/decode/execute cycle.
	 */
	private int macro;

	/**
	 * The instruction to be executed.
	 */
	public int instruction, modifier;

	/**
	 * True if the machine has halted, false if not.
	 */
	private boolean halt;
	
	/**
	 * A String of input from the keyboard.
	 */
	private String port6;

	/**
	 * Initializes the fields of the CPU class.
	 */
	public CPU() {
		Random rand = new Random();

		String[] randomValues = new String[REG_COUNT];
		for (int i = 0; i < REG_COUNT; i++) {
			randomValues[i] = binarizeTheDecimal(rand
					.nextInt(MAX_REG_VALUE + 1));
		}
		reg_file = new RegisterFile(randomValues);

		randomValues = new String[MEM_SIZE];
		for (int i = 0; i < MEM_SIZE; i++) {
			randomValues[i] = binarizeTheDecimal(rand
					.nextInt(MAX_MEM_VALUE + 1));
		}
		mem = new Memory(randomValues);

		randomValues = new String[ALU_REG];
		for (int i = 0; i < ALU_REG; i++) {
			randomValues[i] = binarizeTheDecimal(rand
					.nextInt(MAX_REG_VALUE + 1));
		}
		alu = new Alu(randomValues);

		sw = new Register(binarizeTheDecimal(rand.nextInt(MAX_REG_VALUE)));
		pc = new Register("0000000000000000");
		ir = new Register(binarizeTheDecimal(rand.nextInt(MAX_REG_VALUE)));
		mdr = new Register(binarizeTheDecimal(rand.nextInt(MAX_REG_VALUE)));
		mar = new Register(binarizeTheDecimal(rand.nextInt(MAX_REG_VALUE)));
		macro = 1;
		halt = false;
		port6 = null;
	}

	/**
	 * Executes one state of the fetch/decode/execute cycle.
	 */
	public void step() {
		if (getHalt() == false) {
			switch (macro) {
			case 1:
				fetch();
				break;

			case 2:
				decode();
				break;

			case 3:
				fetchOperands();
				break;

			case 4:
				executeInstruction(instruction);
				instruction = 0;
				modifier = 0;
				break;
			}
		}
	}

	/**
	 * The 1st CPU state, this retrieves the next instruction from memory,
	 * and increments the PC to point to the next instruction.
	 */
	private void fetch() {
		ir.setRegValue(mem.getWord(decifyTheBinary(pc.getRegValue())));

		// increment the pc
		int pcvalue;
		pcvalue = decifyTheBinary(pc.getRegValue());
		pcvalue += 2;
		pc.setRegValue(binarizeTheDecimal(pcvalue));

		setMacro(2);
	}

	/**
	 * The 2nd CPU state, this retrieves the opcode and modifier from the current
	 * instruction and determines whether or not the fetch operands state is needed.
	 */
	private void decode() {

		instruction = Integer.parseInt(ir.getHighByte().substring(0, 5));

		modifier = Integer.parseInt(ir.getRegValue().substring(ADDR_END));

		if (ir.getRegValue().substring(0, 6).startsWith("01")) {
			setMacro(3);
		} else {
			setMacro(4);
		}

	}

	/**
	 * The 3rd CPU state, this retrieves the operands for the current operation,
	 * storing them in the op1 and op2 fields.
	 */
	private void fetchOperands() {
		op1 = (reg_file.getRegister(decifyTheBinary(ir.getRegValue().substring(
				DEST_START, DEST_END))));
		op2 = (reg_file.getRegister(decifyTheBinary(ir.getRegValue().substring(
				DEST_END, ADDR_END))));
		
		alu.setRegA(op1);
		alu.setRegB(op2);
		setMacro(4);
	}

	/**
	 * The 4th CPU state, this executes the operation currently held in the IR.
	 * 
	 * @param instruction the opcode of the instruction.
	 */
	private void executeInstruction(final int instruction) {

		if (ir.getHighByte().startsWith("01")) {
			alu.setOperation(instruction);
			alu.setModifier(modifier);
		}

		String result, address = "";

		switch (instruction) {
		// LDI
		case 1:
			result = ir.getRegValue().substring(DEST_END);
			reg_file.setWord(decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)),
					result);
			break;
		// LDA
		case 10:
			result = mem.getWord(decifyTheBinary(pc.getRegValue()));
			reg_file.setWord(decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)),
					result);
			break;
		// LDB
		case 11:
			switch (modifier) {
			// Register Mode
			case 0:
				result = mem.getByte(decifyTheBinary(pc.getRegValue()));
				reg_file.setByte(
						decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)), 0,
						result);
				break;
			// Base Relative
			case 1:
				result = pc.getRegValue();
				result = result + reg_file.getRegister(14).getRegValue();
				reg_file.setByte(
						decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)), 0,
						mem.getByte(decifyTheBinary(result)));
				break;
			// Index Mode
			case 10:
				result = mem.getByte(decifyTheBinary(reg_file.getRegister(12)
						.getRegValue()));
				reg_file.setByte(
						decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)), 0,
						result);
				reg_file.setByte(
						12,
						0,
						binarizeTheDecimal(decifyTheBinary(reg_file.getWord(12)) + 1));
				break;
			// Indirect Mode
			case 11:
				result = mem.getByte(decifyTheBinary(mem
						.getByte(decifyTheBinary(pc.getRegValue()))));
				reg_file.setByte(
						decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)), 0,
						result);
				break;
			}
			break;
		// LDW
		case 100:
			switch (modifier) {
			// Register Mode
			case 0:
				address = ir.getRegValue().substring(DEST_END, ADDR_END);
				result = mem.getWord(decifyTheBinary(address));
				reg_file.setWord(
						decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)),
						result);
				break;
			// Base Relative
			case 1:
				result = pc.getRegValue();
				result = result + reg_file.getRegister(14).getRegValue();
				reg_file.setWord(
						decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)),
						mem.getWord(decifyTheBinary(result)));
				break;
			// Index Mode
			case 10:
				result = mem.getWord(decifyTheBinary(reg_file.getRegister(12)
						.getRegValue()));
				reg_file.setWord(
						decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)),
						result);
				reg_file.setWord(
						12,
						binarizeTheDecimal(decifyTheBinary(reg_file.getWord(12)) + 1));
				break;
			// Indirect Mode
			case 11:
				result = mem.getWord(decifyTheBinary(mem
						.getWord(decifyTheBinary(pc.getRegValue()))));
				reg_file.setWord(
						decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)),
						result);
				break;
			// MOV
			case 100:
				reg_file.setWord(
						decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)),
						reg_file.getWord(decifyTheBinary(ir.getRegValue()
								.substring(DEST_END, ADDR_END))));
				break;
			}
			break;
		// STB
		case 101:
			switch (modifier) {
			// Register Mode
			case 0:
				result = reg_file.getByte(decifyTheBinary(ir.getRegValue()
						.substring(DEST_START, DEST_END)), 0);
				writeMemory(1,
						decifyTheBinary(reg_file.getByte(decifyTheBinary(ir
								.getRegValue().substring(DEST_END, ADDR_END)), 0)), result);
				break;
			// Base Relative
			case 1:
				result = reg_file.getByte(decifyTheBinary(ir.getRegValue()
						.substring(DEST_START, DEST_END)), 0);
				writeMemory(
						1,
						decifyTheBinary(reg_file.getByte(decifyTheBinary(ir
								.getRegValue().substring(DEST_END, ADDR_END)), 0)
								+ reg_file.getByte(14, 0)), result);
				break;
			// Index Mode
			case 10:
				result = reg_file.getByte(decifyTheBinary(ir.getRegValue()
						.substring(DEST_START, DEST_END)), 0);
				writeMemory(1, decifyTheBinary(reg_file.getWord(13)), result);
				reg_file.setWord(
						13,
						binarizeTheDecimal(decifyTheBinary(reg_file.getWord(13)) + 1));
				break;
			// Indirect Mode
			case 11:
				result = reg_file.getByte(decifyTheBinary(ir.getRegValue()
						.substring(DEST_START, DEST_END)), 0);
				address = mem.getByte(decifyTheBinary(reg_file
						.getWord(decifyTheBinary(ir.getRegValue().substring(DEST_END,
								ADDR_END)))));
				writeMemory(1, decifyTheBinary(address), result);
				break;
			}
			break;
		// STW
		case 110:
			switch (modifier) {
			// Register Mode
			case 0:
				result = reg_file.getWord(decifyTheBinary(ir.getRegValue()
						.substring(DEST_START, DEST_END)));
				writeMemory(2,
						decifyTheBinary(reg_file.getWord(decifyTheBinary(ir
								.getRegValue().substring(DEST_END, ADDR_END)))), result);
				break;
			// Base Relative
			case 1:
				result = reg_file.getWord(decifyTheBinary(ir.getRegValue()
						.substring(DEST_START, DEST_END)));
				writeMemory(
						2,
						decifyTheBinary(reg_file.getWord(decifyTheBinary(ir
								.getRegValue().substring(DEST_END, ADDR_END)))
								+ reg_file.getWord(14)), result);
				break;
			// Index Mode
			case 10:
				result = reg_file.getWord(decifyTheBinary(ir.getRegValue()
						.substring(DEST_START, DEST_END)));
				writeMemory(2, decifyTheBinary(reg_file.getWord(13)), result);
				reg_file.setWord(
						13,
						binarizeTheDecimal(decifyTheBinary(reg_file.getWord(13)) + 1));
				break;
			// Indirect Mode
			case 11:
				result = reg_file.getWord(decifyTheBinary(ir.getRegValue()
						.substring(DEST_START, DEST_END)));
				address = mem.getWord(decifyTheBinary(reg_file
						.getWord(decifyTheBinary(ir.getRegValue().substring(DEST_END,
								ADDR_END)))));
				writeMemory(2, decifyTheBinary(address), result);
				break;
			}
			break;

		case 1001:
			alu.executeOperation();
			if (modifier == 10 || modifier == 11) {
				reg_file.setWord(8, alu.getRegR().getRegValue());
				reg_file.setWord(9, alu.getRegR2().getRegValue());
			} else {
				reg_file.setWord(decifyTheBinary(ir.getRegValue().substring(DEST_START, DEST_END)), alu.getRegR().getRegValue());
			}
			sw = alu.getSW();
			break;

		case 1000:
			alu.executeOperation();
			sw = alu.getSW();
			break;

		case 1010:
			alu.executeOperation();
			sw = alu.getSW();
			break;
		// BR immediate
		case 10000:
			result = ir.getRegValue().substring(DEST_START);
			sext(result);
			pc.setRegValue(binarizeTheDecimal(decifyTheBinary(pc.getRegValue())
					+ decifyTheBinary(result)));
			break;
		// BR register
		case 10001:
			pc.setRegValue(reg_file.getWord(decifyTheBinary(ir.getRegValue()
					.substring(DEST_START, DEST_END))));
			break;
		// BR conditional
		case 10010:
			if (checkBranch(ir.getRegValue().substring(15)) == true) {
				result = ir.getRegValue().substring(DEST_START, 14);
				result = sext(result);
				pc.setRegValue(binarizeTheDecimal(decifyTheBinary(pc
						.getRegValue()) + decifyTheBinary(result)));
			}
			break;
		// BR conditional Register Mode
		case 10011:
			if (checkBranch(ir.getRegValue().substring(15)) == true) {
				result = ir.getRegValue().substring(DEST_START, DEST_END);
				pc.setRegValue(reg_file.getWord(decifyTheBinary(result)));
			}
			break;
		// JSR
		case 10100:
			String temp_string = pc.getRegValue();
			pc.setRegValue(reg_file.getWord(decifyTheBinary(ir.getRegValue()
					.substring(DEST_START, DEST_END))));
			reg_file.setWord(12, temp_string);
			break;
		// JSR Conditional
		case 10101:
			if (checkBranch(ir.getRegValue().substring(15)) == true) {
				reg_file.setWord(12, pc.getRegValue());
				pc.setRegValue(reg_file.getWord(decifyTheBinary(ir
						.getRegValue().substring(DEST_START, DEST_END))));
			}
			break;
		// RET
		case 10111:
			pc.setRegValue(reg_file.getWord(12));
			break;
		// HALT
		case 11010:
			setHalt(true);
			break;
		// I/O
		case 11000:
			switch (modifier) {
			// IN
			case 0:
				if (port6 == null) {
					new ConsoleInput(this);
				}
			break;
			// OUT
			case 1:
				port6 = null;
			}
			break;
		}
		
		setMacro(1);
	}

	/**
	 * The 5th CPU state, this writes the result of the last operation into memory.
	 * 
	 * @param size 1 for byte, 2 for word
	 * @param address the address to write to
	 * @param value the value to write into memory
	 */
	private void writeMemory(final int size, final int address,
			final String value) {
		if (size == 1) {
			mem.setByte(address, value);
		} else {
			mem.setWord(address, value);
		}
	}

	/**
	 * Checks the status word to see if branch conditions are met.
	 * 
	 * @param test the last two bits of the branch instruction
	 * @return true if a branch is needed, false if not
	 */
	public boolean checkBranch(final String test) {
		int value = Integer.parseInt(test);
		Register status = alu.getSW();
		boolean result = false;
		switch (value) {
		case 0:
			if (status.getRegValue().charAt(0) == '1') {
				result = true;
			}
			break;
		case 10:
			if (status.getRegValue().charAt(1) == '1') {
				result = true;
			}
			break;
		case 1:
			if (status.getRegValue().charAt(2) == '1') {
				result = true;
			}
			break;
		case 11:
			if (status.getRegValue().charAt(3) == '1') {
				result = true;
			}
			break;
		}

		return result;
	}

	/**
	 * Sets the current CPU state to the given value.
	 * 
	 * @param newState the new state for the CPU
	 */
	public void setMacro(final int newState) {
		macro = newState;
	}

	/**
	 * Returns the current CPU state.
	 * 
	 * @return the state as an integer
	 */
	public int getMacro() {
		return macro;
	}
	
	/**
	 * Sets the contents of port 6, which stores the KBD input.
	 * 
	 * @param input the String the user entered
	 */
	public void setKeyboardInput(final String input) {
		port6 = input;
	}
	
	/**
	 * Gets the String stored in port 6.
	 * 
	 * @return the contents of port 6.
	 */
	public String getPort6() {
		return port6;
	}

	/**
	 * Converts a binary string to its decimal equivalent.
	 * 
	 * @param value the initial binary string
	 * @return the value as a decimal value
	 */
	public int decifyTheBinary(final String value) {
		int final_value = 0;
		boolean neg = false;
		String temp_string = "";
		temp_string = value;
		for (int i = temp_string.length() - 1; i >= 0; i--) {
			if (temp_string.charAt(i) == '1') {
				final_value += Math.pow(2, temp_string.length() - 1 - i);
			}
		}

		if (neg) {
			final_value++;
			final_value = -final_value;
		}

		return final_value;
	}

	/**
	 * Converts a binary string into its hexadecimal equivalent.
	 * 
	 * @param value the binary string
	 * @return the value as a hex string
	 */
	public String hexifyTheBinary(final String value) {
		String[] binary;

		if (value.length() == 16) {
			binary = new String[4];
		} else {
			binary = new String[2];
		}

		String result = "";
		for (int i = 0; i < binary.length; i++) {
			binary[i] = value.substring(i * 4, i * 4 + 4);
		}

		for (int j = 0; j < binary.length; j++) {
			for (int k = 0; k < BINARY.length; k++) {
				if (binary[j].equals(BINARY[k])) {
					result += HEX[k];
					break;
				}
			}
		}

		return result;
	}

	/**
	 * Converts a decimal value to its hexadecimal equivalent.
	 * 
	 * @param value the initial decimal value
	 * @return that value as a hexadecimal string
	 */
	public String hexifyTheDecimal(final int value) {
		return Integer.toHexString(value);
	}

	/**
	 * Converts a hexadecimal value into its 2's complement
	 * binary equivalent.
	 * 
	 * @param value the hex value
	 * @return the value as a binary string
	 */
	public String binarizeTheHex(final String value) {
		String result = new BigInteger(value, 16).toString(2);
		int length = result.length();
		for (int i = 0; i < 16 - length; i++) {
			result = "0" + result;
		}
		return result;
	}

	/**
	 * Converts a decimal value into its 2's complement binary equivalent.
	 * 
	 * @param value the decimal value
	 * @return the value as a binary string
	 */
	public String binarizeTheDecimal(final int value) {
		if (value < 0) {
			return Integer.toBinaryString(value).substring(16);
		} else {
			return Integer.toBinaryString(value);
		}
	}

	/**
	 * Sets whether or not the CPU has halted using the
	 * given boolean.
	 * 
	 * @param status true if halted, false if not
	 */
	public void setHalt(final boolean status) {
		halt = status;
	}

	/**
	 * Returns whether or not the CPU has halted.
	 * 
	 * @return true if CPU has halted, false if not
	 */
	public boolean getHalt() {
		return halt;
	}

	/**
	 * Takes the given String and sign extends it to be a full 16 bits.
	 * 
	 * @param value the original String to be sexted
	 * @return the new 16 bit String
	 */
	public String sext(final String value) {
		int length = value.length();
		String result = value;
		if (result.charAt(0) == '1') {
			for (int i = 0; i < 16 - length; i++) {
				result = "1" + result;
			}
		} else {
			for (int i = 0; i < 16 - length; i++) {
				result = "0" + result;
			}
		}

		return result;
	}

	/**
	 * Returns the RegisterFile as a string, displaying each Register and their
	 * values.
	 * 
	 * @return the string representation of the RegisterFile.
	 */
	public String registerFileToString() {
		StringBuilder sb = new StringBuilder();

		sb.append("Register File\n");
		for (int i = 0; i < REG_COUNT; i++) {
			sb.append("R" + HEX[i] + ": 0x"
					+ hexifyTheBinary(reg_file.getRegister(i).getRegValue())
					+ "\n");
		}

		return sb.toString();
	}

	/**
	 * Represents a portion of memory as a string.
	 * 
	 * @param start
	 *            the starting point for the block of memory (inclusive).
	 * @param end
	 *            the ending point for the block of memory (inclusive).
	 * @return the block of memory in string form.
	 */
	public String memoryToString(final int start) {
		int temp_start = start;
		StringBuilder sb = new StringBuilder();

		sb.append("Memory Block\n");
		sb.append("Address\tByte\tByte\n");

		if (mem.memory.length - 1 - temp_start < 32) {
			temp_start = mem.memory.length - 1 - 32;
		}

		for (int i = temp_start; i < temp_start + 32; i = i + 2) {
			sb.append("0x");
			sb.append(hexifyTheDecimal(i));
			sb.append("\t");
			sb.append(hexifyTheBinary(mem.getByte(i)));
			sb.append("\t");
			sb.append(hexifyTheBinary(mem.getByte(i + 1)));
			sb.append("\n");
		}

		return sb.toString();
	}

	/**
	 * Returns a String representing the current values in the CPU
	 * registers.
	 * 
	 * @return the String
	 */
	public String cpuToString() {
		StringBuilder sb = new StringBuilder();

		sb.append("CPU Registers\n");
		sb.append("SW - 0x");
		sb.append(hexifyTheBinary(sw.getRegValue()));
		sb.append(", ");
		sb.append("PC - 0x");
		sb.append(hexifyTheBinary(pc.getRegValue()));
		sb.append(", ");
		sb.append("IR - 0x");
		sb.append(hexifyTheBinary(ir.getRegValue()));
		sb.append(", ");
		sb.append("MDR - 0x");
		sb.append(hexifyTheBinary(mdr.getRegValue()));
		sb.append(", ");
		sb.append("MAR - 0x");
		sb.append(hexifyTheBinary(mar.getRegValue()));

		return sb.toString();
	}
}